﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using Utilities;
using Interfaces;
using System.Reflection;

namespace Factory
{
   public class Loader : ILoader
   {
      private static Logger logger_;
      private static Type[] programInitializeTypes_;
      private Dictionary<Type, ConstructorInfo> ctorInfo_;
      private Dictionary<string, Type> types_;
      private Dictionary<MethodInfo, Config.Plugin> programMethods_;

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="logger"></param>
      static Loader()
      {
         programInitializeTypes_ = new Type[] { typeof(Form), typeof(Logger),
            typeof(Configuration), typeof(Loader), typeof(IAlmanac), typeof(ITransport) };
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="logger"></param>
      public Loader()
      {
         logger_ = Program.Logger;
         ctorInfo_ = new Dictionary<Type, ConstructorInfo>();
         types_ = new Dictionary<string, Type>();
         programMethods_ = new Dictionary<MethodInfo, Config.Plugin>();
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="config"></param>
      /// <returns></returns>
      public bool LoadPlugins()
      {
         try
         {
            // Load each of the assemblies listed and see what they hold
            if (!Program.Settings.Plugins.Value.OfType<Config.Plugin>().All(plugin => LoadPlugin(plugin)))
            {
               // already output an error message
               return false;
            }

            return true;
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Now that we have loaded all of the plugins, we can copy the global
      /// objects over to them all
      /// </summary>
      /// <param name="config"></param>
      /// <returns></returns>
      public bool InitializePrograms(Form main, IAlmanac almanac, ITransport transport)
      {
         try
         {
            object[] globals = new object[] { main, logger_, Program.AppConfig,
               this, almanac, transport };
            return programMethods_.All(kvp =>
               {
                  MethodInfo initializeMethod = kvp.Key;
                  Config.Plugin plugin = kvp.Value;
                  try
                  {
                     return (bool)initializeMethod.Invoke(null, globals);
                  }
                  catch (System.Exception ex)
                  {
                     logger_.Exception("exception in plugin '{0}'", ex, plugin.Name);
                  }
                  return false;
               });
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// 
      /// </summary>
      /// <param name="plugin"></param>
      /// <returns></returns>
      private bool LoadPlugin(Config.Plugin plugin)
      {
         try
         {
            Assembly interfacesAssembly = typeof(ILoader).Assembly;
            string pluginName = plugin.Name;
            string typeName = pluginName.StartsWith("Interfaces.") ? pluginName
               : string.Concat("Interfaces.", pluginName);
            Type wantType = interfacesAssembly.GetType(typeName);
            if (null == wantType)
            {
               logger_.ErrorFormat("unknown plugin name '{0}'", pluginName);
               return false;
            }

            Assembly assembly = null;
            string pluginPath = Path.GetFullPath(plugin.Filename);
            logger_.DebugFormat("loading plugin '{0}' from '{1}' == '{2}'", plugin.Name,
               Path.GetFullPath(plugin.Filename), pluginPath);
#if true
            // The simple way but it locks the DLL
            assembly = Assembly.LoadFile(pluginPath);
#else
            // assembly.LoadFile() locks the assembly, which may become a problem later,
            // so we read the DLL and PDB files manually and construct the assembly from
            // those, instead
            string pdbPath = Path.ChangeExtension(pluginPath, "pdb");
            if (File.Exists(pdbPath))
            {
               assembly = Assembly.Load(File.ReadAllBytes(pluginPath), File.ReadAllBytes(pdbPath));
            }
            else
            {
               // There is no PDB file
               logger_.warnFormat("There are no symbols available for '{0}', file '{1}' does not exist", plugin.name, pdbPath);
               assembly = Assembly.Load(File.ReadAllBytes(pluginPath));
            }
#endif
            if (!assembly.GetLoadedModules().All(module => ScanPlugin(plugin, module, wantType)))
            {
               logger_.DebugFormat("filename '{1}' is not a valid plugin (config setting '{0}')",
                  plugin.Name, Path.GetFullPath(plugin.Filename));
               return false;
            }
            return true;
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILoader
      /// </summary>
      private bool ScanPlugin(Config.Plugin plugin, Module module, Type wantType)
      {
         try
         {
            // Look for the type that implements the plugin's object type
            Type type = module.GetType(plugin.Classname, false);
            if (null == type)
            {
               logger_.ErrorFormat("cannot find class '{0}' in file '{1}' for '{2}'",
                  plugin.Classname, Path.GetFullPath(plugin.Filename), plugin.Name);
               return false;
            }
            if (!wantType.IsAssignableFrom(type))
            {
               logger_.ErrorFormat("class '{0}' does not derive from '{3}' in file '{1}' for '{2}'",
                  plugin.Classname, Path.GetFullPath(plugin.Filename), plugin.Name, wantType.FullName);
               return false;
            }

            // Store the constructor for the implementation of the type that we want
            ConstructorInfo ctorInfo = type.GetConstructor(Type.EmptyTypes);
            if (null == ctorInfo)
            {
               logger_.WarnFormat("could not find constructor for type '{2}' in file '{1}' for plugin '{0}'",
                  type.FullName, Path.GetFullPath(plugin.Filename), plugin.Name);
               return false;
            }
            ctorInfo_.Add(wantType, ctorInfo);
            types_.Add(wantType.FullName, wantType);

            // The module must include a Program class that has an initialize() method,
            // so we can pass in the global objects
            if (!LookForProgramInitialize(plugin, module))
            {
               logger_.ErrorFormat("could not find Program.Initialize() in plugin '{0}'; file '{1}'",
                  plugin.Name, Path.GetFullPath(plugin.Filename));
               return false;
            }
            return true;
         }
         catch (System.Exception ex)
         {
            logger_.Exception("exception in plugin '{0}'", ex, plugin.Name);
         }
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// If the assembly contains a static Program class, we can use it
      /// to pass in our Logger and Loader
      /// </summary>
      /// <param name="module"></param>
      private bool LookForProgramInitialize(Config.Plugin plugin, Module module)
      {
         try
         {
            Type type = module.GetType(plugin.Program, false);
            if ((null != type) && type.IsClass)
            {
               MethodInfo method = type.GetMethod("Initialize", programInitializeTypes_);
               if ((null != method) && method.IsStatic && (typeof(bool) == method.ReturnType))
               {
                  // Remember this module for when we want to set the almanacs
                  // Do nothing if we are already set to call this method
                  if (!programMethods_.ContainsKey(method))
                  {
                     programMethods_.Add(method, plugin);
                  }
                  return true;
               }
            }
         }
         catch (System.Exception ex)
         {
            logger_.Exception("exception in plugin '{0}'", ex, plugin.Name);
         }
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILoader
      /// </summary>
      public bool CanCreate(Type type)
      {
         try
         {
            return (null != type) && ctorInfo_.ContainsKey(type);
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILoader
      /// </summary>
      public object Create(Type type)
      {
         try
         {
            return ctorInfo_[type].Invoke(null);
         }
         catch (System.Exception ex)
         {
            logger_.Exception("exception trying to create an instance of '{0}'", ex, type.FullName);
         }
         return null;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILoader
      /// </summary>
      public bool CanCreate(string typeName)
      {
         try
         {
            return !string.IsNullOrEmpty(typeName) &&
               types_.ContainsKey(typeName) && CanCreate(types_[typeName]);
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILoader
      /// </summary>
      public object Create(string typeName)
      {
         try
         {
            return ctorInfo_[types_[typeName]].Invoke(null);
         }
         catch (System.Exception ex)
         {
            logger_.Exception("exception trying to create an instance of '{0}'", ex, typeName);
         }
         return null;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILoader
      /// </summary>
      public bool CanCreate<TYPE>() where TYPE : class
      {
         try
         {
            return ctorInfo_.ContainsKey(typeof(TYPE));
         }
         catch (System.Exception ex)
         {
            logger_.Exception(ex);
         }
         return false;
      }

      ////////////////////////////////////////////////////////////////////////
      /// <summary>
      /// Implementation of ILoader
      /// </summary>
      public TYPE Create<TYPE>() where TYPE : class
      {
         try
         {
            return ctorInfo_[typeof(TYPE)].Invoke(null) as TYPE;
         }
         catch (System.Exception ex)
         {
            logger_.Exception("exception trying to create an instance of '{0}'", ex, typeof(TYPE).FullName);
         }
         return null;
      }
   }
}
